# decoding=utf-8
# -*- coding: utf-8 -*-
import csv
import random
import string
import time
from collections import deque

from flask import Flask, session, request, abort
from flask_cors import CORS
import sys
import hashlib
import os
import redis
import json
from flask_sqlalchemy import SQLAlchemy

from sqlalchemy import Column, Integer, String, DateTime, func
import traceback

from werkzeug.exceptions import BadRequest

import config

# import logging

# logging.basicConfig()
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

app = Flask(__name__)
app.config.from_object(config.DevConfig)
db = SQLAlchemy(app)
record_table_mapper = {}
CORS(app)
redis_pool = redis.ConnectionPool(host='reform-redis', port=6379)

reload(sys)
sys.setdefaultencoding("utf-8")

reform_dir = '/home/data/reform'
code_dir = '/home/data/reform/code'
exec_dir = '/home/data/reform/exec'


class NoMissingDict(dict):
    def __missing__(self, key):
        abort(400)


class User(db.Model):
    __tablename__ = 'users'

    def __init__(self, id, password, salt):
        self.id = id
        self.password = password
        self.salt = salt

    id = Column(Integer, primary_key=True)
    password = Column(String(64), nullable=False)
    salt = Column(String(32), nullable=False)
    device = Column(Integer, nullable=False, default=0)
    user_group = Column(Integer, nullable=False, default=1)
    register_time = Column(DateTime, nullable=False, server_default=func.now())


# API
@app.route('/api/user/login', methods=['POST'])
def api_user_login():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")

    user_id = request_json['id']
    password_user = request_json['password']

    try:
        user = User.query.filter(User.id == user_id).filter(User.device != -1).first()
        if user is None:
            return make_status_false("user_not_exist")

        password_db = user.password

        if hashlib.sha256(password_user + user.salt).hexdigest() == password_db:
            # 验证通过,生成并分发token
            token = os.urandom(24).encode('base-64')[:-1]
            # 解决redis里多出\n的问题

            # 通过python操作redis缓存

            r = redis.Redis(connection_pool=redis_pool)
            tokens = r.get("reform.tokens.{}".format(user.id))
            if not tokens:
                r.set("reform.tokens.{}".format(user.id), json.dumps([token]))
            else:
                tokens = deque(json.loads(tokens))
                while len(tokens) > 10:
                    t = tokens.pop()
                    r.delete(t)
                tokens.appendleft(token)
                r.set("reform.tokens.{}".format(user.id), json.dumps(list(tokens)))

            r.set(token, str(user_id), ex=604800)
            session['token'] = token
            return json.dumps({
                "status": True,
                "data": {
                    "token": token,
                    "user_group": user.user_group,
                    "device": user.device
                }
            })
        else:
            return make_status_false("wrong_password")

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


@app.route('/api/user/info', methods=['POST'])
def api_user_info():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)
    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        return json.dumps({
            "status": True,
            "data": {
                "id": user.id,
                "user_group": user.user_group,
                "device": user.device
            }
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


@app.route('/api/user/logout', methods=['POST'])
def api_user_logout():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    r.delete(token)
    session.pop('token', None)

    return json.dumps({
        "status": True
    })


# API
# token
@app.route('/api/user/register', methods=['POST'])
def api_user_register():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    target_user_id = request_json['target_user_id']
    if "target_user_password" in request_json:
        password_user = request_json['target_user_password']
        if len(password_user) < 6:
            return make_status_false("password_format_error")
    else:
        password_user = None

    try:
        if "device" in request_json:
            device = int(request_json['device'])
            if device > 20 or device < 0:
                return make_status_false("device_number_err")
        else:
            device = None
    except ValueError, e:
        return make_status_false("format_error")

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")
    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only administrator can register users
        if user.user_group != 0:
            return make_status_false("user_group_not_allowed")

        # search for target user

        target_user = User.query.filter_by(id=target_user_id).first()

        if password_user:
            salt = os.urandom(24).encode('base-64')[:-1]
            password_db = hashlib.sha256(password_user + salt).hexdigest()

        if target_user is None:
            # user not exist, register
            revise = False
            if not password_user:
                abort(400)
            if not device:
                device = 0
            else:
                device_dup = User.query.filter(User.device == device).first()
                if device_dup:
                    return make_status_false("device_duplicated")

            target_user = User(target_user_id, password_db, salt)
            target_user.device = device
            db.session.add(target_user)
            db.session.commit()
        elif target_user.device == -1:
            # user deleted, register
            revise = False
            if not password_user:
                abort(400)
            if not device:
                target_user.device = 0
            else:
                device_dup = User.query.filter(User.device == device).filter(User.id != target_user.id).first()
                if device_dup:
                    return make_status_false("device_duplicated")
                target_user.device = device

            target_user.password = password_db
            target_user.salt = salt
            db.session.commit()

        else:
            # user exist, revise
            revise = True
            revised = False
            if device:
                device_dup = User.query.filter(User.device == device).filter(User.id != target_user.id).first()
                if device_dup:
                    return make_status_false("device_duplicated")
                set_idle_and_remove_files(target_user.device)
                set_idle_and_remove_files(device)

                target_user.device = device
                revised = True
            if password_user:
                target_user.password = password_db
                target_user.salt = salt
                tokens = r.get("reform.tokens.{}".format(target_user.id))
                if tokens:
                    for token in json.loads(tokens):
                        r.delete(token)
                    r.set("reform.tokens.{}".format(target_user.id), "[]")
                revised = True
            if revised:
                db.session.commit()
            else:
                abort(400)
        return json.dumps({
            "status": True,
            "data": "revise" if revise else "register"
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


@app.route('/api/user/list', methods=['POST'])
def api_user_list():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only administrator can do this
        if user.user_group != 0:
            return make_status_false("user_group_not_allowed")

        users = User.query.filter(User.user_group == 1).filter(User.device != -1).all()
        users_all = [{"id": user.id,
                      "device": user.device,
                      "register_time": int(time.mktime(user.register_time.timetuple()))}
                     for user in users]
        return json.dumps({
            "status": True,
            "data": users_all
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


@app.route('/api/user/delete', methods=['POST'])
def api_user_delete():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only administrator can do this
        if user.user_group != 0:
            return make_status_false("user_group_not_allowed")

        delete_users = request_json["users"]
        res = []

        for delete_user in delete_users:
            u = User.query.filter(User.user_group == 1).filter(User.device != -1).filter(User.id == delete_user).first()
            if u:
                res.append({"id": delete_user, "status": True, "msg": ""})
                u.device = -1
                tokens = r.get("reform.tokens.{}".format(u.id))
                if tokens:
                    for token in json.loads(tokens):
                        r.delete(token)
                    r.set("reform.tokens.{}".format(u.id), "[]")
            else:
                res.append({"id": delete_user, "status": False, "msg": "invalid_user"})
        db.session.commit()

        return json.dumps({
            "status": True,
            "data": res
        })
    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


# API
# token
@app.route('/api/device/status_all', methods=['POST'])
def api_device_status_all():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only administrator can do this
        if user.user_group != 0:
            return make_status_false("user_group_not_allowed")

        return json.dumps({
            "status": True,
            "data": get_status_all()
        })
    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


# API
# token
@app.route('/api/device/status', methods=['POST'])
def api_device_status():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only normal user can do this
        if user.user_group != 1:
            return make_status_false("user_group_not_allowed")

        if user.device == 0:
            return make_status_false("device_not_binded")

        status_all = get_status_all()

        if not (0 <= user.device - 1 < len(status_all)):
            return make_status_false("internal_error_device_number_out_of_bound")

        return json.dumps({
            "status": True,
            "data": status_all[user.device - 1]
        })
    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


# API
# token
@app.route('/api/code/get', methods=['POST'])
def api_code_get():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only normal user can do this
        if user.user_group != 1:
            return make_status_false("user_group_not_allowed")

        if user.device == 0:
            return make_status_false("device_not_binded")

        with open(os.path.join(code_dir, str(user.device), "origin.cpp"), 'r') as f:
            code = "".join(f.readlines())

        return json.dumps({
            "status": True,
            "data": code
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except IOError, e:
        print(traceback.format_exc())
        return json.dumps({
            "status": True,
            "data": ""
        })
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


# API
# token
@app.route('/api/code/submit', methods=['POST'])
def api_code_submit():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)
    code = request_json['code']

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        # only normal user can do this
        if user.user_group != 1:
            return make_status_false("user_group_not_allowed")

        if user.device == 0:
            return make_status_false("device_not_binded")

        if not (0 <= user.device - 1 < 20):
            return make_status_false("internal_error_device_number_out_of_bound")

        with open(os.path.join(code_dir, str(user.device), "origin.cpp"), 'w') as f:
            f.write(code)

        r.set("reform.code_status.{}".format(user.device), "submitted")

        return json.dumps({
            "status": True
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


# API
# token
@app.route('/api/user/history', methods=['POST'])
def api_user_history():
    try:
        request_json = json.loads(request.get_data(), object_hook=NoMissingDict)
    except ValueError, e:
        return make_status_false("format_error")
    token = get_token(request_json)

    r = redis.Redis(connection_pool=redis_pool)
    user_id = r.get(token)
    if user_id is None:
        return make_status_false("need_login")

    try:
        user = User.query.filter_by(id=user_id).first()
        if user is None:
            return make_status_false("user_not_exist")

        rate_limit = r.get("reform.download_limit.{}".format(user_id))
        if rate_limit is not None:
            return make_status_false("rate_limit")

        r.set("reform.download_limit.{}".format(user_id), "", ex=10)

        fake_conn = db.engine.raw_connection()
        fake_cur = fake_conn.cursor()
        if user.user_group == 0:
            # 0:admin user
            file_name = "history_{}.csv".format(time.strftime('%Y%m%d%H%M%S', time.gmtime(time.time()+8*3600)))
            with open(os.path.join('/tmp/reform', file_name), 'wb') as f:
                copy_sql = 'COPY (select * from history) TO STDOUT WITH CSV HEADER'
                fake_cur.copy_expert(copy_sql, f)

        else:
            assert user.user_group == 1
            # 1:normal user
            if user.device == 0:
                return make_status_false("device_not_binded")

            if not (0 <= user.device - 1 < 20):
                return make_status_false("internal_error_device_number_out_of_bound")
            file_name = "history_{}_{}.csv".format(user_id, time.strftime('%Y%m%d%H%M%S', time.gmtime(time.time()+8*3600)))
            with open(os.path.join('/tmp/reform', file_name), 'wb') as f:
                copy_sql = 'COPY (select * from history_{}) TO STDOUT WITH CSV HEADER'.format(user_id)
                fake_cur.copy_expert(copy_sql, f)

        return json.dumps({
            "status": True,
            "data": "/download/" + file_name
        })

    except BadRequest, e:
        print(traceback.format_exc())
        return make_status_false("bad_request")
    except Exception, e:
        print(traceback.format_exc())
        return make_status_false("unknown")


def get_token(request_json):
    if "token" in request_json:
        token = request_json['token']
    elif "token" in session:
        token = session['token']
    else:
        abort(403)
    return token


def get_status_all():
    r = redis.Redis(connection_pool=redis_pool)
    status_read = r.get("reform.status_read")

    if status_read:
        status_read = json.loads(status_read)
    else:
        status_read = [{} for _ in range(0, 20)]

    def get_pwm(device):
        pwm = r.get("reform.pwm.{}".format(device))
        if pwm:
            return int(pwm)
        else:
            return 0

    def get_code_status(device):
        code_status = r.get("reform.code_status.{}".format(device))
        if code_status:
            return code_status
        else:
            return "idle"

    def get_code_status_detail(device):
        code_status_detail = r.get("reform.code_status_detail.{}".format(device))
        if code_status_detail:
            return code_status_detail
        else:
            return ""

    status_all = [{"device": i + 1,
                   "generator_i": status_read[i]["generator_i"] if "generator_i" in status_read[i] else 0,
                   "generator_u": status_read[i]["generator_u"] if "generator_u" in status_read[i] else 0,
                   "payload_i": status_read[i]["payload_i"] if "payload_i" in status_read[i] else 0,
                   "payload_u": status_read[i]["payload_u"] if "payload_u" in status_read[i] else 0,
                   "pwm": get_pwm(i + 1),
                   "code_status": get_code_status(i + 1),
                   "code_status_detail": get_code_status_detail(i + 1)
                   } for i in range(0, 20)]

    return status_all


def make_status_false(msg):
    return json.dumps({
        "status": False,
        "msg": msg
    })


def create_dirs():
    print("try create dirs")
    if not os.path.isdir(code_dir):
        os.makedirs(code_dir)
    if not os.path.isdir(exec_dir):
        os.makedirs(exec_dir)

    for i in range(1, 21):
        target_dir = os.path.join(code_dir, str(i))
        if not os.path.isdir(target_dir):
            os.makedirs(target_dir)
        target_dir = os.path.join(exec_dir, str(i))
        if not os.path.isdir(target_dir):
            os.makedirs(target_dir)


def set_idle_and_remove_files(device):
    r = redis.Redis(connection_pool=redis_pool)
    r.set("reform.code_status.{}".format(device), "idle")
    r.set("reform.code_status_detail.{}".format(device), "")
    r.delete("reform.device_user.{}".format(device))
    origin_code_filename = os.path.join(code_dir, str(device), "origin.cpp")
    gen_code_filename = os.path.join(code_dir, str(device), "gen.cpp")
    exec_filename = os.path.join(exec_dir, str(device), str(device))
    if os.path.exists(origin_code_filename):
        os.remove(origin_code_filename)
    if os.path.exists(gen_code_filename):
        os.remove(gen_code_filename)
    if os.path.exists(exec_filename):
        os.remove(exec_filename)


app.secret_key = '&ASt\xae\x08r\xee\xa5\xef\xaf\x02z\xeco\xe3B\x9a\xb5M\xb6(\xd7\xc8'
create_dirs()
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=app.config["UWSGI_PORT"])
